'use strict'

const { readFileSync } = require('fs')
const { parse } = require('jest-docblock')

const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
const log = require('../../dd-trace/src/log')

/**
 * There are two ways to call `test.each` in `jest`:
 * 1. With an array of arrays: https://jestjs.io/docs/api#1-testeachtablename-fn-timeout
 * 2. With a tagged template literal: https://jestjs.io/docs/api#2-testeachtablename-fn-timeout
 * This function distinguishes between the two and returns the test parameters in different formats:
 * 1. An array of arrays with the different parameters to the test, e.g.
 * [[1, 2, 3], [2, 3, 5]]
 * 2. An array of objects, e.g.
 * [{ a: 1, b: 2, expected: 3 }, { a: 2, b: 3, expected: 5}]
 */
function getFormattedJestTestParameters (testParameters) {
  if (!testParameters || !testParameters.length) {
    return
  }
  const [parameterArray, ...parameterValues] = testParameters
  if (parameterValues.length === 0) { // Way 1.
    return parameterArray
  }
  // Way 2.
  const parameterKeys = parameterArray[0].split('|').map(key => key.trim())
  const formattedParameters = []
  let lastFormattedParameter = {}
  for (let index = 0; index < parameterValues.length; index++) {
    const parameterIndex = index % parameterKeys.length
    if (parameterIndex === 0) {
      lastFormattedParameter = {}
      formattedParameters.push(lastFormattedParameter)
    }
    const key = parameterKeys[parameterIndex]
    lastFormattedParameter[key] = parameterValues[index]
  }

  return formattedParameters
}

// Support for `@fast-check/jest`: this library modifies the test name to include the seed
// A test name that keeps changing breaks some Test Optimization's features.
const SEED_SUFFIX_RE = /\s*\(with seed=-?\d+\)\s*$/i
// https://github.com/facebook/jest/blob/3e38157ad5f23fb7d24669d24fae8ded06a7ab75/packages/jest-circus/src/utils.ts#L396
function getJestTestName (test, shouldStripSeed = false) {
  const titles = []
  let parent = test
  do {
    titles.unshift(parent.name)
  } while ((parent = parent.parent))

  titles.shift() // remove TOP_DESCRIBE_BLOCK_NAME

  const testName = titles.join(' ')
  if (shouldStripSeed) {
    return testName.replace(SEED_SUFFIX_RE, '')
  }
  return testName
}

const globalDocblockRegExp = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/
const MAX_COMMENTS_CHECKED = 10

function isMarkedAsUnskippable (test) {
  let testSource

  try {
    testSource = readFileSync(test.path, 'utf8')
  } catch {
    return false
  }

  const re = globalDocblockRegExp
  re.lastIndex = 0
  let commentsChecked = 0

  while (testSource.length) {
    const match = re.exec(testSource)
    if (!match) break
    const comment = match[1]

    let docblocks
    try {
      docblocks = parse(comment)
    } catch {
      // Skip unparsable comment and continue scanning
      if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
        return false
      }
      continue
    }

    if (docblocks?.datadog) {
      try {
        // @ts-expect-error The datadog type is defined by us and may only be a string.
        return JSON.parse(docblocks.datadog).unskippable
      } catch {
        // If the @datadog block comment is present but malformed, we'll run the suite
        log.warn('@datadog block comment is malformed.')
        return true
      }
    }

    if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
      return false
    }

    // To stop as soon as no doc blocks are found, slice the source. That way the
    // regexp works by using the `^` anchor. Without it, it would continue
    // scanning the rest of the file.
    testSource = testSource.slice(match[0].length)
  }

  return false
}

function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
  const unskippableSuites = {}
  const forcedToRunSuites = {}

  const skippedSuites = []
  const suitesToRun = []

  for (const test of originalTests) {
    const relativePath = getTestSuitePath(test.path, rootDir)
    const shouldBeSkipped = skippableSuites.includes(relativePath)
    if (isMarkedAsUnskippable(test)) {
      suitesToRun.push(test)
      unskippableSuites[relativePath] = true
      if (shouldBeSkipped) {
        forcedToRunSuites[relativePath] = true
      }
      continue
    }
    if (shouldBeSkipped) {
      skippedSuites.push(relativePath)
    } else {
      suitesToRun.push(test)
    }
  }

  const hasUnskippableSuites = Object.keys(unskippableSuites).length > 0
  const hasForcedToRunSuites = Object.keys(forcedToRunSuites).length > 0

  if (originalTests.length) {
    // The config object is shared by all tests, so we can just take the first one
    const [test] = originalTests
    if (test?.context?.config?.testEnvironmentOptions) {
      if (hasUnskippableSuites) {
        test.context.config.testEnvironmentOptions._ddUnskippable = JSON.stringify(unskippableSuites)
      }
      if (hasForcedToRunSuites) {
        test.context.config.testEnvironmentOptions._ddForcedToRun = JSON.stringify(forcedToRunSuites)
      }
    }
  }

  return {
    skippedSuites,
    suitesToRun,
    hasUnskippableSuites,
    hasForcedToRunSuites
  }
}

module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun, isMarkedAsUnskippable }
